import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Random;
import java.util.Scanner;
import java.util.Vector;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.OneShotBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPANames;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.FailureException;
import jade.domain.FIPAAgentManagement.NotUnderstoodException;
import jade.domain.FIPAAgentManagement.RefuseException;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.proto.ContractNetInitiator;
import jade.proto.ContractNetResponder;



public class Trader extends Agent {
	private AID facilID = new AID("Facilitator", AID.ISLOCALNAME);	// Facilitator AID
	private Hashtable<String,Double> productGenerator;				// product building probabilities
	private Hashtable<String,Product> stock;						// current stock info
	private Hashtable<String,BuyHabit> buyHabits;
	private ArrayList<String> buyTypes;								// types of products the agent is looking for
	private int budget;												// total budget
	private int budgetStep;											// budget surplus at every product generation
	private CapitalistWorld.Quality productQuality; 				// different quality for each product
	private CapitalistWorld.Strategy sellingStrategy;				// the selling strategy
	private double unfaithfulSeller;								// probability of being unfaithful seller
	private double unfaithfulBuyer;									// probability of being unfaithful buyer
	
	private ArrayList<AID> traders;									// traders in the system
	
	private final long PRODUCTION_RATE = 6000;						// frequency of producing resources (milis)
	private final long BUY_RATE = 3000;								// frequency of buying resources (milis)
	private final long REPUTATION_UPDATE_RATE = 3000;				// frequency of reputation updates
	private final String UNFAITHFUL_MESSAGE = "teapa";				// message sent in case of unfaithful seller
	
//	private ArrayList<Model> sellerModels;							// trust and reputation models for sellers
	private ArrayList<Model> buyerModels;							// trust and reputation models for buyers
	
	protected void setup() {
		print("is ready.");
		
		// Register the trader-agent service in the yellow pages
		DFAgentDescription dfd = new DFAgentDescription();
		dfd.setName(getAID());
		ServiceDescription sd = new ServiceDescription();
		sd.setType("trader-agent");
		sd.setName("JADE-product-trading");
		dfd.addServices(sd);
		try {
			DFService.register(this, dfd);
		}
		catch (FIPAException fe) {
			fe.printStackTrace();
			doDelete();
		}
		
		// Setup the agent
		addBehaviour(new RequestInitData());		
	}
	
	protected void takeDown() {
		print("terminating.");
	}
	
	private void updateTraders() {
		// Get the list of registered traders.
		DFAgentDescription template = new DFAgentDescription();
		ServiceDescription sd = new ServiceDescription();
		sd.setType("trader-agent");
		template.addServices(sd);
		try {
			DFAgentDescription[] result = DFService.search(this, template); 
			traders = new ArrayList<AID>();
			for (int i = 0; i < result.length; ++i) {
				// Remove myAgent.
				if (result[i].getName().getLocalName().compareTo(getAID().getLocalName()) == 0) continue;
				traders.add(result[i].getName());
			}
//			print("Found the following traders: " + traders.toString());
			
		}
		catch (FIPAException fe) {
			fe.printStackTrace();
			doDelete();
		}
	}
	
	private void initModels() {
		if (traders == null) return;
		
//		sellerModels = new ArrayList<Model>();
		for (String producType : stock.keySet()) {
			Product p = stock.get(producType);
			p.addModel(new TRModel(this, traders, 0.75));
		}
		buyerModels = new ArrayList<Model>();
		
		// TODO: init models
//		sellerModels.add(new TRModel(this, traders, 0.75));
		buyerModels.add(new TRModel(this, traders, 1));
	}
	
	private void updateSellerModelsTrust(String productType, AID trader, int interaction, double modifier) {
		ArrayList<Model> sellerModels = stock.get(productType).getModels();
		
		for (Model m : sellerModels) {
			m.updateTrust(trader, interaction, modifier);
			print("[" + interaction + ", " + modifier + "] Seller trust for " + trader.getLocalName() + ": " + m.getTrust(trader));
		}
	}
	
	private void updateBuyerModelsTrust(String productType, AID trader, int interaction, double modifier) {
		ArrayList<Model> sellerModels = stock.get(productType).getModels();
		
		for (Model m : sellerModels) {
			m.updateTrust(trader, interaction, modifier);
			print("[" + interaction + "] Buyer trust for " + trader.getLocalName() + ": " + m.getTrust(trader));
		}
	}
	
	private void updateBuyerModelsTrust(String productType, AID trader, int interaction) {
		updateBuyerModelsTrust(productType, trader, interaction, 0);
	}
	
	private void updateModelsReputation() {
		for (AID aid : traders) {
			for (String productType : stock.keySet()) {
				ArrayList<Model> sellerModels = stock.get(productType).getModels(); 
				for (Model m : sellerModels) {
					m.updateReputation(aid);
				}
			}
			for (Model m : buyerModels) {
				m.updateReputation(aid);
			}
		}
	}
	
	private void print(String message) {
		System.out.println("Trader " + getAID().getLocalName() + ": " + message);
	}
	
	private class RequestInitData extends OneShotBehaviour {

		/**
		 * Parse the initialization message received from the Facilitator.
		 * 
		 * @param message
		 */
		private void unpackInitData(String message) {
			if (message == null) {
				print("failed to initialize.");
				myAgent.doDelete();
			}
			
			Scanner s = new Scanner(message);
			
			budget = s.nextInt();
			budgetStep = s.nextInt();
			unfaithfulSeller = s.nextDouble();
			unfaithfulBuyer = s.nextDouble();
			
			productGenerator = new Hashtable<String,Double>();
			stock = new Hashtable<String,Product>();
			int n = s.nextInt();
			for (int i = 0; i < n; ++ i) {
				String type = s.next();
				productGenerator.put(type, s.nextDouble());
				stock.put(type, new Product(type, s.nextInt(), s.nextInt()));
			}
			
			buyTypes = new ArrayList<String>();
			buyHabits = new Hashtable<String, BuyHabit>();
			while (s.hasNext()) {
				String type = s.next();
				buyHabits.put(type, new BuyHabit(type, s.nextInt(), s.nextInt()));
			}
			
			s.close();
			
			print("budget = " + budget);
			print("stepBudget = " + budgetStep);
			print("unfaithfullSeller = " + unfaithfulSeller);
			print("unfaithfullBuyer = " + unfaithfulBuyer);
			print("productGenerator = "+productGenerator.toString());
			print("product stocks = "+stock.toString());
			print("buyingHabits"+buyHabits.toString());
		}
		
		@Override
		public void action() {
			// Send request to Facilitator.
			ACLMessage message = new ACLMessage(ACLMessage.REQUEST);
			message.addReceiver(facilID);
			message.setConversationId("init-data");
			myAgent.send(message);

			// Wait for reply with the initialization data.
			ACLMessage reply = myAgent.blockingReceive(MessageTemplate.MatchConversationId("init-data"));
			unpackInitData(reply.getContent());
			print("done initializing.");
			
			// All done. Start agent activity.
			addBehaviour(new StartAgentBehaviour());
		}
	}
	
	private class StartAgentBehaviour extends OneShotBehaviour {

		/**
		 * Check if there are resources available.
		 * 
		 * @param productType
		 * @return true if resources are available
		 */
		private boolean evaluateCFP(String productType) {
			Product p = stock.get(productType);	
			return (p != null && p.getStock() > 0);
		}
		
		/**
		 * Sell product. If available, update stock and budget.
		 * 
		 * @param productType
		 * @return true if product is still available
		 */
		private boolean satisfyOrder(AID buyer, boolean fake, String productType, int price, String unfaithful) {
			if (!evaluateCFP(productType)) return false;
			
			// not unfaithful seller
			if (!fake) {
				Product p = stock.get(productType);
				p.decStock();
			}
			
			// unfaithful buyer
			if (unfaithful != null && (unfaithful.compareTo(UNFAITHFUL_MESSAGE) == 0)) {
				// penalize buyer
				updateBuyerModelsTrust(productType, buyer, 0);
			}
			else {
				budget += price;
				updateBuyerModelsTrust(productType, buyer, 1);
			}
			
			return true;
		}
		
		@Override
		public void action() {
			
			// init traders list
			updateTraders();
			// init trust and reputation models
			initModels();
			
/*			ArrayList<AID> testAgents = new ArrayList<AID>();
			AID testAID1;
			AID testAID2;
			if (myAgent.getAID().getLocalName().compareTo("Trader2") == 0) {
				testAID1 = new AID("Trader1", AID.ISLOCALNAME);
				testAID2 = new AID("Trader3", AID.ISLOCALNAME);
			}
			else if (myAgent.getAID().getLocalName().compareTo("Trader1") == 0) {
				testAID1 = new AID("Trader2", AID.ISLOCALNAME);
				testAID2 = new AID("Trader3", AID.ISLOCALNAME);
			} else {
				testAID1 = new AID("Trader2", AID.ISLOCALNAME);
				testAID2 = new AID("Trader1", AID.ISLOCALNAME);				
			}
			testAgents.add(testAID1);
			testAgents.add(testAID2);
			
			Model m = new TRModel(myAgent, testAgents, 0.75);
			
			m.updateTrust(testAID1, 1, 0);
			m.updateTrust(testAID1, 0, 0);
			m.updateTrust(testAID1, 1, 0);
			
			m.updateTrust(testAID2, 0, 0);
			m.updateTrust(testAID2, 1, 0);
			m.updateTrust(testAID2, 0, 0);
			
			print("trust for " + testAID1.getLocalName() + " " + m.getTrust(testAID1));
			print("trust for " + testAID2.getLocalName() + " " + m.getTrust(testAID2));
			
			print("Taking a sleep");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			m.updateReputation(testAID1, 0);
			m.updateReputation(testAID2, 0);*/
			
			// respond to buy requests
			MessageTemplate template = MessageTemplate.and(
					MessageTemplate.MatchProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET),
					MessageTemplate.MatchPerformative(ACLMessage.CFP) );
			
			addBehaviour(new ContractNetResponder(myAgent, template) {
				
				/**
				 * Prepare the proposal.
				 * The format of the proposal is: "<productType> <price>".
				 */
				//TODO trader could promise a better quality then the actual one
				protected ACLMessage prepareResponse(ACLMessage cfp) throws NotUnderstoodException, RefuseException {
					print("CFP received from "+cfp.getSender().getName()+". Action is "+cfp.getContent());
					if (evaluateCFP(cfp.getContent())) {
						// We provide a proposal
						ACLMessage propose = cfp.createReply();
						propose.setPerformative(ACLMessage.PROPOSE);
						propose.setContent(cfp.getContent() + " " + stock.get(cfp.getContent()).getPrice());
  						return propose;
  					}
  					else {
  						// We refuse to provide a proposal
  						print("Refuse.");
  						throw new RefuseException("evaluation-failed");
  					}
  				}
  				
				/**
				 * Handle order.
				 * The inform message has the format: "<productType> <price> <productQuality>".
				 * If the seller is unfaithful, the UNFAITHFUL_MESSAGE is sent instead.
				 */
  				protected ACLMessage prepareResultNotification(ACLMessage cfp, ACLMessage propose,ACLMessage accept) throws FailureException {
  					print("Proposal accepted");
  					boolean fake = CapitalistWorld.checkThreshold(unfaithfulSeller);
  					
  					Scanner s = new Scanner(accept.getContent());
  					String productType = s.next();
  					int price = s.nextInt();
  					String unfaithful = null;
  					if (s.hasNext()) unfaithful = s.next();
  					s.close();
  					
  					if (satisfyOrder(accept.getSender(), fake, productType, price, unfaithful)) {
  						ACLMessage inform = accept.createReply();
  						inform.setPerformative(ACLMessage.INFORM);
  						if (fake) {
  	 						print("I am unfaithful seller.");
  							inform.setContent(UNFAITHFUL_MESSAGE + " " + productType);
  						}
  						else {
  							print("I am faithful seller.");
  							inform.setContent(productType + " " + price + " " + stock.get(productType).getQuality());
  						}
  						return inform;
  					}
  					else {
  						print("Action execution failed");
  						throw new FailureException("unexpected-error");
  					}	
  				}
  			} );
			
			// generate products
			addBehaviour(new GenerateProducts(myAgent, PRODUCTION_RATE));
			// buy products
			addBehaviour(new BuyProducts(myAgent, BUY_RATE));
			
			addBehaviour(new TickerBehaviour(myAgent, REPUTATION_UPDATE_RATE) {

				@Override
				protected void onTick() {
					updateModelsReputation();
				}
				
			});
		}
	}
	
	private class GenerateProducts extends TickerBehaviour {

		public GenerateProducts(Agent a, long period) {
			super(a, period);
		}
		
		/**
		 * Generate products according to probabilities in productGenerator.
		 */
		private void generateProducts() {
			Iterator<String> genIt = productGenerator.keySet().iterator();
			while (genIt.hasNext()) {
				String type = genIt.next();
				// TODO: update quality and price for each product based on the selling strategy
				if (CapitalistWorld.checkThreshold(productGenerator.get(type))) {
					Product p = stock.get(type);
					p.incStock();
				}
			}
			
			budget += budgetStep;
			
			print("done generating => " + stock.toString());
		}

		@Override
		protected void onTick() {
			print("generating products.");
			generateProducts();
		}
	}
	
	private class BuyProducts extends TickerBehaviour {

		public BuyProducts(Agent a, long period) {
			super(a, period);
		}
		
		private double evaluateProposal(String proposal) {
			// TODO set agent's weight for proposals
			double w = 0.5;
			
			Scanner s = new Scanner(proposal);
			s.next();	// skip product type
			int price = s.nextInt();
			s.close();
			
			if (price > budget) return -1;
			
			return w*price;
		}
		
		/**
		 * Start negotiations for products I am interested in.
		 */
		private void buyProducts() {
			for (String bType : buyTypes) {
				ACLMessage msg = new ACLMessage(ACLMessage.CFP);
		  		for (AID aid : traders) {
		  			msg.addReceiver(aid);
		  		}
				msg.setProtocol(FIPANames.InteractionProtocol.FIPA_CONTRACT_NET);
				// We want to receive a reply in 10 secs
				msg.setReplyByDate(new Date(System.currentTimeMillis() + 10000));
				msg.setContent(bType);
				
				// Start the actual negotiation.
				addBehaviour(new ContractNetInitiator(myAgent, msg) {
					private int reserved;
					private int nResponders = traders.size();
					
					/**
					 * Received proposal.
					 */
					protected void handlePropose(ACLMessage propose, Vector v) {
						print("Agent "+propose.getSender().getName()+" proposed "+propose.getContent());
					}
					
					/**
					 * Handle all the proposals.
					 */
					protected void handleAllResponses(Vector responses, Vector acceptances) {
						if (responses.size() < nResponders) {
							// Some responder didn't reply within the specified timeout
							print("Timeout expired: missing "+(nResponders - responses.size())+" responses");
						}
						
						// Evaluate proposals.
						double bestProposal = -1;
						AID bestProposer = null;
						String proposalMessage = null;
						ACLMessage accept = null;
						Enumeration e = responses.elements();
						while (e.hasMoreElements()) {
							ACLMessage msg = (ACLMessage) e.nextElement();
							if (msg.getPerformative() == ACLMessage.PROPOSE) {
								ACLMessage reply = msg.createReply();
								reply.setPerformative(ACLMessage.REJECT_PROPOSAL);
								acceptances.addElement(reply);
								double proposal = evaluateProposal(msg.getContent());
								if (proposal > bestProposal) {
									bestProposal = proposal;
									bestProposer = msg.getSender();
									proposalMessage = msg.getContent();
									accept = reply;
								}
							}
						}
						// Accept the proposal of the best proposer
						if (accept != null) {
							Scanner s = new Scanner(proposalMessage);
							s.next();	// skip product type
							int price = s.nextInt();
							s.close();
							
							print("Accepting proposal "+proposalMessage+" from responder "+bestProposer.getName());
							accept.setPerformative(ACLMessage.ACCEPT_PROPOSAL);
							
							// if the buyer is unfaithful, we will not deduce the cost from our budget
							if (CapitalistWorld.checkThreshold(unfaithfulBuyer)) {
								print("I am unfaithful buyer.");
								proposalMessage += " " + UNFAITHFUL_MESSAGE;
							}
							else {
								print("I am faithful buyer.");
								budget -= price;
								reserved = price;
							}
							
							accept.setContent(proposalMessage);
						}
					}
					
					/**
					 * Handle inform messages.
					 */
					protected void handleInform(ACLMessage inform) {
						Scanner s = new Scanner(inform.getContent());
						String productType = s.next();
						
						if (productType.equals(UNFAITHFUL_MESSAGE)) {
							// penalize bad seller
							productType = s.next();
							updateSellerModelsTrust(productType, inform.getSender(), 0, 0);
							return;
						}
						
						int price = s.nextInt();
//						CapitalistWorld.Quality q = CapitalistWorld.Quality.valueOf(s.next());
						int quality = s.nextInt();
						s.close();
						
						// evaluate seller based on satisfaction function
						updateSellerModelsTrust(productType, inform.getSender(), 1, (double) quality / price);
						
						print("Agent "+inform.getSender().getName()+" successfully performed the requested action");
					}
					
					/**
					 * Handle failures (e.g. in case the product is not available anymore).
					 */
					protected void handleFailure(ACLMessage failure) {
						if (failure.getSender().equals(myAgent.getAMS())) {
							// FAILURE notification from the JADE runtime: the receiver
							// does not exist
							print("Responder does not exist");
						}
						else {
							budget += reserved;
							print("Agent "+failure.getSender().getName()+" failed. Rolled back budget.");
						}
						// Immediate failure --> we will not receive a response from this agent
						nResponders --;
					}
				});
			}
		}

		@Override
		protected void onTick() {
			
			// update traders list
			updateTraders();
			
			// Start buying products.
			buyProducts();
		}
		
	}
}
